<template>
    <view class="nh-cascader" v-if="visible">
        <view class="cascader-mask" :class="{'show-mask': animate}"></view>
        <view class="cascader-container flex-column" :class="{'component-display': animate}">
            <!-- 此部分为组件头部的渲染，从左往右依次为'取消','以选中x条数据，确定' 并有各自的触发事件-->
            <view v-if="header" class="header flex-row">
                    <view class="cancel btn flex-row" @click="cancel">
                        <text>取消</text>
                        <view class="flex1"></view>
                    </view>
                    <view class="flex1" style="text-align: center">
                        {{title ? title : `已选中${selectedArray.length}条数据`}}
                    </view>
                    <view class="sure btn flex-row" @click="confirm">
                        <view class="flex1"></view>
                        <text>确定</text>
                    </view>
            </view>
            <!-- 父级组件若带的props里search值为true则证明显示搜索框 -->
            <view v-if="search" class="search-body flex-row">
                <!-- 放大镜标识，使用Bese64编码 -->
                <img class="search-icon" src="" alt="">
                <!-- v-model绑定keyword，并使用trim取消输入的空格 -->
                <input v-model.trim="keyword" class="search-input flex1" type="text" :placeholder="placeHolder ? placeHolder : '请输入关键字...'">
                <!-- 只有输入框中有值才会展示，点击该图标(删除图标)，输入的值为空，图片使用base64编码 -->
                <img v-if="keyword" @click="keyword = ''" class="search-icon" src="" alt="">
            </view>
            <!-- 若父级组件传过来的数据长度为0(假)则会展示‘没有数据’ -->
            <view v-if="!treeData.length" class="data-tips">
                <text>没有数据！</text>
            </view>
            <!-- 此滚动区域实际上是当你在搜索框中输入时显示的区域，存在面包屑。并添加了loadMore方法 -->
            <scroll-view v-else-if="keyword" :scroll-y="true" class="search-list" @scrolltolower="loadMore">
                <!-- 此处为一条一条的数据，里面包含 父级 > 子级  选择框  这样的内容 -->
                <!-- 核心内容1 -->
                <view class="bread-crumb flex-row" @click="selectItem(item)" v-for="item in searchList" :key="item[valueKey]">
                    <view class="flex1 flex-row">
                        <view class="parent-name" v-if="item.parent && item.parent[labelKey]" v-html="highLigit(item.parent[labelKey], item.checked ? '#007aff' : '#333')"></view>
                        <view v-html="highLigit(item[labelKey], item.checked ? '#007aff' : '#333')"></view>
                    </view>
                    <!-- 右侧选择框，点击后样式生效，判断是复选而且是叶子节点（没有子级）才会展示 -->
                    <div v-if="!(!multiple && !item.isLeaf)" class="click-contraler">
                        <view class="check-box" :class="{'radio-box': !multiple}">
                            <view class="item-checked" :class="{'item-radio': !multiple}" v-if="item.checked"></view>
                        </view>
                    </div>
                </view>
                <!-- 数据条数为0时展示 -->
                <view class="data-tips" v-if="!searchList.length">
                    <text>没有符合条件的数据！</text>
                </view>
                <!-- 除去上面的那种情况，若当前数据长度小于总长度展示 -->
                <view class="data-tips load-more" v-else-if="searchList.length < searchLength">
                    <text>上划加载更多 {{`${searchList.length} / ${searchLength}`}}</text>
                </view>
                <!-- 除去上面的情况，若搜索框中有值，且数据加载完全展示 -->
                <view class="data-tips" v-else-if="keyword">
                    <text>搜索结果{{searchLength}}条已全部展示</text>
                </view>
            </scroll-view>
            <!-- 此滚动区域是当你没有输入关键字时的默认展示的树状结构区域 -->
            <scroll-view v-else :scroll-y="true" class="tree-source">
                <!-- flex-row是每个树状图下展的内容，根据level的不同，所以margin-left也不同，这里是使用每一层margin-left相比父级多了20px来进行控制 -->
                <view class="flex-row node-item"
                        v-for="node in displayList"
                        :style="{marginLeft: `${node.level * 20}px`}"
                        :key="node[valueKey]">
                    
                    <view @click="extendItem(node)" class="flex-row">
                        <view v-if="node.isLeaf" class=empty-node></view>
                        <!-- 左侧小三角图标，使用base64进行编码 -->
                        <img v-else
                            class="node-leaf"
                            :class="{open: node.open}"
                            src="" alt="">
                            <!-- 节点名称 -->
                            <view class="node-name" :class="{active: node.checked}">{{node[labelKey]}}</view>
                    </view>
                    <!-- flex布局节点 -->
                    <view class="flex1"></view>
                    <!-- 状态框 -->
                    <view v-if="!(!multiple && !node.isLeaf)" @click="selectItem(node)" class="click-contraler">
                        <view class="check-box" :class="{'radio-box': !multiple}">
                            <!-- 当前节点是选中状态 -->
                            <view class="item-checked" :class="{'item-radio': !multiple}" v-if="node.checked"></view>
                            <!-- 若当前节点只选中了部分节点，那么则展示选中一半的样式 -->
                            <view class="item-half-checked" v-if="node.half"></view>
                        </view>
                    </view>
                </view>
            </scroll-view>
        </view>
    </view>
</template>

<script>
  export default {
        props: {
            treeData: {
                required: true,
                type: Array
            },
            // 初始化值
            value: {
                type: [Array, String],
                required: false
            },
            // 是否显示搜索框
            search: {
                type: Boolean,
                required: false,
                default: true
            },
            // 是否显示header
            header: {
                type: Boolean,
                required: false,
                default: true
            },
            // 是否为复选
            multiple: {
                type: Boolean,
                required: false,
                default: false
            },
            // 选自器标题
            title: {
                type: String,
                required: false,
                default: ''
            },
            // 占位文字
            placeHolder: {
                type: String,
                required: false,
                default: ''
            },
            // 主键键名
            valueKey: {
                type: String,
                required: false,
                default: 'id'
            },
            // 展示的键名
            labelKey: {
                type: String,
                required: false,
                default: 'name'
            },
            // 分页，传入FALSE则不分页
            pageSize: {
                type: [Number, Boolean],
                required: false,
                default: 50
            }
        },
        data() {
            return {
                visible: false,
                animate: false,
                listData: [],
                keyword: '',
                pageNum: 1,
                searchLength: 0,
                timer:  null
            };
        },
        methods: {
            open () {
                this.visible = true
                // 出示化关闭期间values变化的值
                this.init(this.value)
                setTimeout(() => {
                    this.animate = true
                }, 0)
            },
            close() {
                this.visible = false
                this.animate = false
            },
            //当下滚到底部时，<scorrll-view>自带的触发方法，scrolltolower：下拉触发
            loadMore() {
                //若此时数据的长度小于总长度，则延迟300毫秒执行页数自加的操作
                if (this.searchList.length < this.searchLength) {
                    setTimeout(() => {
                        this.pageNum++
                    }, 300)
                }
            },
            treeToArray(treeData) {
                let list = []
                const {valueKey} = this
                if (isNaN(parseInt(treeData.length))) {
                    return []
                }
                function count (tree, level, parent, seniorIdsArr) {
                    level++
                    seniorIdsArr.push(parent[valueKey])
                    for(let i = 0; i<tree.length; i++) {
                        let {children, ...item} = tree[i]
                        item.open = !!item.open
                        // 将空值主键替换成一个占位符，便于后期处理
                        item[valueKey] = item[valueKey] || 'palceHold_All'
                        const isLeaf = !(children && children.length)
                        list.push({
                            parent,
                            seniorIds : seniorIdsArr.join(','),
                            seniorIdsArr,
                            ...item,
                            level,
                            isLeaf
                        })
                        if (children && children.length) {
                            count(children, level, item, [...seniorIdsArr])
                        }
                    }
                }
                count(treeData, -1, {open: true, [valueKey]: ''}, [])
                return list
            },
            extendItem(node) {
                const {open} = node
                this.listData = this.listData.map(item => {
                    if (open) {
                        // 收起
                        if (item[this.valueKey] === node[this.valueKey] || item.seniorIdsArr.indexOf(node[this.valueKey]) > -1) {
                            item.open = false
                        }
                    } else {
                        if (item[this.valueKey] === node[this.valueKey]) {
                            item.open = true
                        }
                    }
                    return item
                })
            },
            selectItem(node) {
                const {checked} = node
                const {valueKey} = this
                // 单选走这条
                if (!this.multiple) {
                    //如果是叶子节点
                    if (node.isLeaf) {
                        this.listData = this.listData.map(item => {
                            //如果选中的是当前节点
                            if (item[valueKey] === node[valueKey]) {
                                //改变当前节点的选中状态
                                item.checked = !checked
                            } else {
                                //否则将节点状态置为false
                                item.checked = false
                            }
                            // half是用来标记当前节点的状态，若当前节点全选则置为false
                            item.half = false
                            return item
                        })
                    }
                    return
                }
                // 子节点
                let newList = this.listData.map(item => {
                    // 子节点
                    if (item[valueKey] === node[valueKey] || item.seniorIdsArr.indexOf(node[valueKey]) > -1) {
                        item.checked = !checked
                        item.half = false
                    }
                    return item
                })

                // 父节点，三元运算符判断当前值是否为空，若为空则返回空数组
                let seniors = node.seniorIdsArr.length > 0 ? node.seniorIdsArr.map(id => {
                    return {[valueKey]: id}
                }) : []
                for (let i = 0; i < seniors.length; i++) {
                    let parent = seniors[i]
                    //这里使用了过滤器，用来判断是否有子节点
                    let children = newList.filter(item => {
                        return item.seniorIdsArr.indexOf(parent[valueKey]) > -1
                    })
                    //同样使用过滤器，用来判断当前子节点的选中状态
                    let checkedChildren = children.filter(item => {
                        if (item[valueKey] === node[valueKey]) {
                            return !checked
                        }
                        return item.checked
                    })
                    //若选中子节点的长度为0
                    if (checkedChildren.length === 0) {
                        //修改父级的选中状态为false
                        parent.checked = false
                        parent.half = false
                        //若当前节点下的孩子节点全部选中，父级也要选中，同时parent.half要置为false
                    } else if (checkedChildren.length === children.length ){
                        parent.checked = true
                        parent.half = false
                        //否则当前选中状态为false
                    } else {
                        parent.checked = false
                        //half是用来标记当前节点的状态，若当前节点全选则置为false
                        parent.half = true
                    }
                }
                this.listData = newList.map(item => {
                    seniors.forEach(senior => {
                        if (senior[valueKey] === item[valueKey]) {
                            item.checked = senior.checked
                            item.half = senior.half
                        }
                    })
                    return item
                })
            },
            //文本高亮方法
            highLigit(text, color) {
                //获取到输入框中的内容
                let keyword = this.keyword
                //使用正则表达式，进行全局匹配
                let reg = new RegExp(`${keyword}`,'g');
                //创建html模板字符串，对text里的内容进行匹配，并替换默认替换成红色的，否则就替换成`color`色
                let str = text.replace(reg, `<span style="color: red">${keyword}</span>`)
                if (color) {
                    str = `<div style="color: ${color}">${str}</div>`
                }
                //返回str
                return str
            },
            //confirm方法，自定义方法，供其他组件使用时调用
            confirm() {
                this.$emit('confirm', this.selectedArray)
                this.close()
            },
            //cancel方法，自定义方法，供其他组件使用时调用
            cancel() {
                this.$emit('cancel', this.selectedArray)
                this.close()
            },
            //初始化
            init(data) {
                // 没传值不重新初始化
                if(!data) return
                // 节流
                if (this.timer) {
                    clearTimeout(this.timer)
                }
                this.timer = setTimeout(() => {
                    try {
                        let str = typeof data === 'string' ? data.split(',') : data
                        let vals = []
                        this.listData = this.listData.map(item => {
                            if (item[this.valueKey] && str.indexOf(item[this.valueKey]) > -1) {
                                vals.push(item)
                            }
                            return {
                                ...item,
                                checked: false,
                                half: false
                            }
                        })
                        vals.forEach(item => {
                            this.selectItem({...item, checked: false})
                        })
                    } catch(e) {
                        console.error(e)
                        console.warn('参数格式错误！')
                    }
                }, 100)
            }
        },
        watch: {
            keyword: {
                handler(N) {
                    this.pageNum = 1
                },
                immediate: false,
                deep: false
            },
            treeData: {
                handler () {
                    this.listData = this.treeToArray(this.treeData)
                    this.init(this.value)
                },
                immediate: true,
                deep: true
            },
            value: {
                handler(N) {
                    if (this.visible) {
                        this.init(N)
                    }
                },
                immediate: true,
                deep: true
            },
            selectedArray: {
                handler(N) {
                    this.$emit('change', N)
                },
                immediate: false,
                deep: true
            }
        },
        computed: {
            displayList() {
                let openItems = this.listData.filter(item => {
                    return item.open || item.level === 0
                })
                return this.listData.filter((item) => {
                    let mark = false
                    for (let i = 0; i < openItems.length; i++) {
                        let parent = openItems[i]
                        if ((item.parent.id === parent.id && parent.open) || item.id === parent.id) {
                            mark = true
                            return mark
                        }
                    }
                    return mark
                })
            },
            selectedArray () {
                return this.listData.filter(item => {
                    return item.checked
                }).map(item => {
                    // 将占位主键值还原
                    item[this.valueKey] === 'palceHold_All' ? '' : item[this.valueKey]
                    return item
                })
            },
            searchList () {
                const label = this.labelKey, keyword = this.keyword
                this.searchLength = 0
                return this.listData.filter((item) => {
                    let mark = false
                    if (item[label]?.indexOf(keyword) > -1 || item?.parent[label]?.indexOf(keyword) > -1) {
                        this.searchLength++
                        if (!this.pageSize || this.searchLength <= (this.pageSize * this.pageNum)) {
                            mark = true
                        }
                    }
                    return mark
                })
            }
        }
    };
</script>

<style lang="scss" scoped>
    .nh-cascader {
        .cascader-mask {
            position: fixed;
            top: 0;
            right: 0;
            bottom: 0;
            left: 0;
            z-index: 9998;
            background-color: rgba(0, 0, 0, 0.6);
            opacity: 0;
            visibility: hidden;
            transition: opacity 0.3s ease;
        }
        .show-mask {
            opacity: 1;
            visibility: visible;
        }
        .component-display {
            height: 80vh !important;
        }
        .cascader-container {
            position: fixed;
            transition: height 0.3s ease;
            bottom: 0;
            left: 0;
            right: 0;
            z-index: 9999;
            background-color: $uni-text-color-inverse;
            padding: 0 24rpx;
            height: 0;
            .header {
                height: 90rpx;
                align-items: center;
                font-size: 32rpx;
                .btn {
                    text-align: center;
                    line-height: 90 Wrpx;
                    color: $uni-color-primary;
                    width: 200rpx;
                    height: 90rpx;
                    align-items: center;
                }
                .cancel {
                    color: $uni-text-color-placeholder;
                }
            }
            .search-body {
                background-color: $uni-text-color-inverse;
                flex-shrink: 0;
                height: 64rpx;
                justify-content: center;
                align-items: center;
                padding: 0 20rpx;
                .search-icon {
                    width: 35rpx;
                    height: 35rpx;
                }
                .search-input {
                    padding-left: 20rpx;
                    font-size: 26rpx;
                    input {
                        line-height: 40rpx !important;
                    }
                }
            }
            .search-list {
                font-size: 26rpx;
                padding-top: 10rpx;
                overflow: auto;
                .bread-crumb {
                    margin-top: 20rpx;
                    line-height: 52rpx;
                    align-items: center;
                    .flex-row {
                        flex-wrap: wrap;
                    }
                    .parent-name {
                        display: flex;
                        align-items: center;
                        &:after {
                            content: '';
                            display: block;
                            width: 40rpx;
                            height: 52rpx;
                            background-image: url("");
                            background-size: 40rpx 40rpx;
                            background-repeat: no-repeat;
                            background-position: center;
                        }
                    }
                }
                /*.load-more {
                    color: $uni-color-primary;
                    margin-bottom: 20rpx;
                }*/
            }
            .tree-source {
                font-size: 26rpx;
                line-height: 52rpx;
                overflow: auto;
                .node-item {
                    align-items: center;
                    margin: 10rpx 0;
                    .node-leaf {
                        margin: 10rpx;
                        width: 30rpx;
                        height: 30rpx;
                        transition: rotate 5s;
                    }
                    .open {
                        transform: rotate(90deg);
                    }
                    .empty-node {
                        margin: 10rpx;
                        width: 30rpx;
                        height: 30rpx;
                    }
                }
            }
            .data-tips {
                color: $uni-text-color-grey;
                line-height: 52rpx;
                text-align: center;
                width: 100%;
            }
            .active {
                color: $uni-color-primary;
            }
            .click-contraler {
                padding: 10rpx;
                .check-box {
                    border: 1px solid $uni-color-primary;
                    width: 34rpx;
                    height: 34rpx;
                    border-radius: 20%;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    .item-checked {
                        width: 18rpx;
                        height: 18rpx;
                        border-radius: 30%;
                        background: $uni-color-primary;
                    }
                    .item-half-checked {
                        width: 18rpx;
                        height: 4rpx;
                        border-radius: 30%;
                        background: $uni-color-primary;
                    }
                }
                .radio-box {
                    border-radius: 50% !important;
                    .item-radio {
                        border-radius: 50% !important;
                    }
                }
            }
        }
        .flex-row {
            display: flex;
            flex-direction: row;
        }
        .flex-column {
            display: flex;
            flex-direction: column;
        }
        .flex1 {
            flex: 1
        }
    }
</style>
